library(readr)
library(dplyr)
library(plotly)
library(scales)
library(RColorBrewer)
library(htmltools)
library(htmlwidgets)
library(jsonlite)
library(stringr)
library(DT)
library(glue)
library(knitr)
library(gt)Project Poster: HDI vs GDP Per Capita Visualization
1 . Loading libraries
2 . Data Loading
df <- read.csv("https://ourworldindata.org/grapher/human-development-index-vs-gdp-per-capita.csv?v=1&csvType=full&useColumnShortNames=true")3 . Data Cleaning & Transformation
3.1 Cleaning the Data
# 1. Start with the raw data
orig_n <- nrow(df)
# 2. Filter to the year range
df1 <- df |>
filter(Year >= 1990, Year <= 2023)
after_year_n <- nrow(df1)
cat(glue("✔ Kept {after_year_n} rows between 1990 and 2023 (dropped {orig_n - after_year_n}).\n"))✔ Kept 9457 rows between 1990 and 2023 (dropped 50095).
# 3. Keep only proper 3-letter codes
invalid_codes <- df1 |>
filter(!str_detect(Code, "^[A-Z]{3}$")) |>
pull(Code) |>
unique()
df2 <- df1 |>
filter(str_detect(Code, "^[A-Z]{3}$"))
after_code_n <- nrow(df2)
cat(glue("✔ Kept {after_code_n} rows with 3-letter codes; dropped {after_year_n - after_code_n} rows (codes: {toString(invalid_codes)}).\n"))✔ Kept 8087 rows with 3-letter codes; dropped 1370 rows (codes: , OWID_AKD, OWID_AUH, OWID_CZS, OWID_GDR, OWID_ERE, OWID_KRU, OWID_KOS, OWID_RVN, OWID_SRM, OWID_USS, OWID_GFR, OWID_WRL, OWID_YAR, OWID_YPR, OWID_YGS).
# 4. Rename & drop rows missing HDI or GDP
df_clean <- df2 |>
rename(
hdi = "hdi__sex_total",
gdp = "ny_gdp_pcap_pp_kd"
)
before_drop_na <- nrow(df_clean)
df_clean <- df_clean |>
filter(!is.na(hdi), !is.na(gdp))
after_drop_na <- nrow(df_clean)
cat(glue("✔ Renamed columns; dropped {before_drop_na - after_drop_na} rows missing HDI or GDP.\n"))✔ Renamed columns; dropped 2339 rows missing HDI or GDP.
# 4.5 Fill in owid_region from each country’s 2023 observation
region_lookup <- df_clean |>
filter(Year == 2023) |>
select(Entity, owid_region) |>
distinct()
df_clean <- df_clean |>
select(-owid_region) |>
left_join(region_lookup, by = "Entity") |>
relocate(owid_region, .after = population_historical)
cat(glue("✔ Filled `owid_region` for all countries based on 2023 data.\n"))✔ Filled `owid_region` for all countries based on 2023 data.
# 5. Final summary
cat(glue("✅ Final cleaned dataset: {nrow(df_clean)} rows, {ncol(df_clean)} columns.\n"))✅ Final cleaned dataset: 5748 rows, 7 columns.
3.2 Cleaned Data Summary
# Render as interactive table
DT::datatable(
df_clean,
caption = "Final Cleaned Dataset",
rownames = FALSE,
options = list(
pageLength = 10,
lengthChange = FALSE,
autoWidth = TRUE
)
)3.3 Engineering New Features (Flagging Development Status & Banding)
df_flagged <- df_clean |>
mutate(
dev_status = if_else(
hdi >= 0.80 & gdp > 12536,
"Developed",
"Developing"
)
)
df_banded <- df_flagged |>
mutate(
# HDI categories: Low (<0.55), Medium (0.55–0.70), High (0.70–0.80), Very High (≥0.80)
hdi_band = case_when(
hdi < 0.55 ~ "Low",
hdi < 0.70 ~ "Medium",
hdi < 0.80 ~ "High",
TRUE ~ "Very High"
),
# GDP per capita categories (World Bank income groups)
gdp_band = case_when(
gdp < 1086 ~ "Low-income",
gdp < 4255 ~ "Lower-middle",
gdp < 13206 ~ "Upper-middle",
TRUE ~ "High-income"
)
)3.4 Banded Data Summary
Showing band changes for: South Korea, United Arab Emirates, Bangladesh
3.5 Statistics by Year
stats_by_year <- df_banded |>
group_by(Year, dev_status) |>
summarize(
count = n(), # number of country‐observations
avg_hdi = mean(hdi), # mean HDI
avg_gdp = mean(gdp), # mean GDP per capita
hdi_min = min(hdi), # minimum HDI
hdi_median = median(hdi), # median HDI
hdi_max = max(hdi), # maximum HDI
gdp_min = min(gdp), # minimum GDP per capita
gdp_median = median(gdp), # median GDP per capita
gdp_max = max(gdp), # maximum GDP per capita
.groups = "drop"
)
stats_by_year |>
slice_head(n = 8) |>
datatable(
options = list(
dom = "t",
autoWidth = TRUE
),
rownames = FALSE
)# Uncomment to save the transformed data and stats
# write.csv(df_banded, "data/transformed_data.csv", row.names = FALSE)
# write.csv(stats_by_year, "data/stats.csv", row.names = FALSE)4 . Improving Visualization
4.1 Final preparation (Post Proposal)
# 1) Load & prep scatter data
df <- read_csv("data/transformed_data.csv", show_col_types = FALSE) |>
filter(!is.na(owid_region)) |>
mutate(
hover_text = paste0(
"<b>", Entity, "</b><br>",
"GDP: $", comma(round(gdp)), "<br>",
"HDI: ", round(hdi, 3), "<br>",
"Pop: ", comma(round(population_historical)), "<br>",
"Status: ", dev_status
)
)
# 2) Load per‑year stats (Year, dev_status, count, avg_gdp, avg_hdi)
stats <- read_csv("data/stats.csv", show_col_types = FALSE)
years <- sort(unique(df$Year))
years_json <- toJSON(years, auto_unbox = TRUE)
stats_json <- toJSON(stats, dataframe = "rows", auto_unbox = TRUE)
# 3) Create country mapping for milestone events - only multi-country events
# Using multiple naming variations to match different dataset conventions
country_groups <- list(
"1991" = c("Russia", "Ukraine", "Belarus", "Kazakhstan", "Georgia", "Armenia", "Azerbaijan", "Estonia", "Latvia", "Lithuania"),
"1997" = c("Thailand", "Indonesia", "South Korea", "Malaysia", "Philippines", "Singapore", "Hong Kong"),
"2001" = c("Argentina", "Brazil", "Uruguay", "Paraguay", "Ecuador"),
"2008" = c("Iceland", "Ireland", "Greece", "Portugal", "Spain", "Italy"),
"2012" = c("Greece", "Spain", "Portugal", "Italy", "Cyprus", "Ireland"),
"2014" = c("Russia", "Ukraine", "Belarus", "Kazakhstan"),
"2015" = c("Greece", "Germany", "France", "Italy", "Spain", "Portugal"),
"2020" = c("India", "Brazil", "Mexico", "Iran", "Peru", "Colombia", "South Africa"),
"2022" = c("Sri Lanka", "Pakistan", "Turkey", "Argentina", "Lebanon")
)
country_groups_json <- toJSON(country_groups, auto_unbox = TRUE)
# 4) Prepare background HDI/GDP bands
hdi_bands <- tibble::tibble(
ymin = c(0,0.55,0.70,0.80),
ymax = c(0.55,0.70,0.80,1.00),
fill = c("#f7fbff","#deebf7","#9ecae1","#3182bd")
)
cuts <- c(1,1086,4255,13206, max(df$gdp,na.rm=TRUE))
gdp_bands <- tibble::tibble(
xmin = cuts[-length(cuts)],
xmax = cuts[-1],
fill = c("#fff5f0","#fee0d2","#fc9272","#de2d26")
)4.2 Drawing Shapes & Annotations
# 5) Prepare Shapes & Annotations
gdp_shapes <- lapply(seq_len(nrow(gdp_bands)), function(i){
list(type="rect", xref="x",
x0=gdp_bands$xmin[i], x1=gdp_bands$xmax[i],
yref="paper", y0=0, y1=1,
fillcolor=gdp_bands$fill[i],
line=list(width=0),
layer="below")
})
hdi_shapes <- lapply(seq_len(nrow(hdi_bands)), function(i){
list(type="rect", xref="paper",
x0=0, x1=1,
yref="y", y0=hdi_bands$ymin[i], y1=hdi_bands$ymax[i],
fillcolor=hdi_bands$fill[i],
line=list(width=0),
layer="below")
})
threshold_shapes <- list(
# Vertical dotted line up to the meeting point
list(
type = "line",
x0 = 12536, x1 = 12536,
y0 = 0, y1 = 0.8,
line = list(
dash = "dot",
width = 2,
color = "black"
)
),
# Horizontal dotted line up to the meeting point
list(
type = "line",
x0 = 1, x1 = 12536,
y0 = 0.8, y1 = 0.8,
line = list(
dash = "dot",
width = 2,
color = "black"
)
)
)
max_gdp <- max(df$gdp, na.rm=TRUE)
hdi_annots <- list(
list(x = -0.11, xref = "paper", y = 0.275, yref = "y", text = "Low HDI", showarrow = FALSE, xanchor = "right", yanchor = "middle", font = list(size=13)),
list(x = -0.11, xref = "paper", y = 0.625, yref = "y", text = "Medium HDI", showarrow = FALSE, xanchor = "right", yanchor = "middle", font = list(size=13)),
list(x = -0.11, xref = "paper", y = 0.75, yref = "y", text = "High HDI", showarrow = FALSE, xanchor = "right", yanchor = "middle", font = list(size=13)),
list(x = -0.11, xref = "paper", y = 0.9, yref = "y", text = "Very High HDI", showarrow = FALSE, xanchor = "right", yanchor = "middle", font = list(size=13))
)
gdp_annots <- list(
list(
x = mean(c(1, 1086)), y = -0.045, text = "Low income", showarrow = FALSE, xanchor = "center", yanchor="top", font = list(size=12)
),
list(
x = mean(c(1086, 4255)), y = -0.045, text = "Lower-middle income", showarrow = FALSE, xanchor = "center", yanchor="top", font = list(size=12)
),
list(
x = mean(c(4255, 13206)), y = -0.045, text = "Upper-middle income", showarrow = FALSE, xanchor = "center", yanchor="top", font = list(size=12)
),
list(
x = mean(c(13206, max_gdp)), y = -0.045, text = "High income", showarrow = FALSE, xanchor = "center", yanchor="top", font = list(size=12)
)
)
axis_band_annotations <- c(hdi_annots, gdp_annots)4.3 Building the Animated Scatter Plot
# 6) Build the animated scatter, Assembling
palette <- brewer.pal(8, "Set2")
p <- plot_ly(
df,
x = ~gdp, y = ~hdi,
frame = ~Year,
type = "scatter", mode = "markers",
color = ~owid_region, colors = palette,
size = ~population_historical, sizes = c(5,500),
text = ~hover_text, hoverinfo = "text",
marker = list(opacity=0.8, line=list(color="black",width=1))
)
# 7) Finalize layout
widget <- p |>
animation_opts(frame=800, transition=300, redraw=FALSE) |>
layout(
title = list(
text = "HDI vs GDP per Capita: Global Trends Over Time",
x = 0.5,
font = list(size = 22, family = "Arial", color = "#222")
),
shapes = c(gdp_shapes, hdi_shapes, threshold_shapes),
annotations = axis_band_annotations,
xaxis = list(
type = "log",
title = "GDP per capita (international-$, 2021 prices)",
tickvals = c(1000,5000,10000,50000,100000),
ticktext = c("$1k","$5k","$10k","$50k","$100k")
),
yaxis = list(
title = "Human Development Index",
range = c(0,1),
tickvals = seq(0,1,0.1)
),
margin = list(l = 155, b = 90, t = 50),
legend = list(title=list(text="Region")),
updatemenus = list() # Remove the top buttons
) |>
config(displayModeBar=FALSE)4.4 Building Widgets with Javascript
# 8) Enhanced JavaScript with unified animation control and country highlighting
widget <- widget |>
htmlwidgets::onRender("
function(el, x) {
var gd = document.getElementById(el.id);
var stats = window.stats_panel_data;
var years = window.years_panel;
var countryGroups = window.country_groups_data;
// Updated milestone data with only multi-country events
var milestoneYears = [1991, 1997, 2001, 2008, 2012, 2014, 2015, 2020, 2022];
var milestoneTexts = {
1991: 'USSR Collapse:<br>Transition from communist to<br>market economies, showing rapid<br>development changes<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Former Soviet states</span>',
1997: 'Asian Financial Crisis:<br>\"Tiger economies\" face severe<br>setbacks, showing vulnerability<br>of emerging markets<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: East/Southeast Asian countries</span>',
2001: 'Argentina Economic Crisis:<br>Middle-income country faces<br>severe economic collapse,<br>highlighting development fragility<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Latin American countries</span>',
2008: 'Global Financial Crisis:<br>Even wealthy nations struggle,<br>but developing countries<br>face disproportionate impact<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Most affected developed nations</span>',
2012: 'European Debt Crisis:<br>Sovereign debt crisis spreads<br>across multiple European nations,<br>threatening the Eurozone<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Crisis-affected European countries</span>',
2014: 'Oil Price Collapse:<br>Commodity-dependent economies<br>struggle while diversified<br>economies remain stable<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Oil-dependent nations</span>',
2015: 'European Migration Crisis:<br>Refugee crisis affects multiple<br>European nations, straining<br>resources and social cohesion<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Major destination countries</span>',
2020: 'COVID-19 Pandemic:<br>Developing nations face<br>disproportionate health and<br>economic impacts vs wealthy nations<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Most affected developing nations</span>',
2022: 'Debt Crisis Wave:<br>Multiple developing nations<br>face sovereign debt crises<br>while rich nations remain stable<br><span style=\"color:#d62728;font-weight:bold;\">→ Highlighted: Crisis-affected nations</span>'
};
var isPlaying = false;
var currentYearIndex = 0;
var animationTimeout;
var allYears = [];
var isTimelineMode = false; // Track which mode we're in
var originalMarkerData = null; // Store original marker properties
// Extract all years from data
if (years && years.length) {
allYears = years.slice(); // Copy array
}
// Function to stop any running animation
function stopAnimation() {
if (isPlaying) {
isPlaying = false;
clearTimeout(animationTimeout);
// Only reset highlighting if we're NOT in timeline mode on a milestone year
var currentYear = allYears[currentYearIndex - 1] || allYears[0]; // Get current year
var shouldKeepHighlighting = isTimelineMode &&
milestoneYears.includes(currentYear) &&
countryGroups[currentYear.toString()] &&
countryGroups[currentYear.toString()].length > 0;
if (!shouldKeepHighlighting) {
resetHighlighting();
}
// Reset all button texts
var timelineBtn = document.getElementById('timeline-btn');
var playBtn = document.getElementById('play-btn');
if (timelineBtn && timelineBtn.textContent.includes('Pause')) {
timelineBtn.textContent = '▶ Play Timeline';
}
if (playBtn && playBtn.textContent.includes('Pause')) {
playBtn.textContent = '▶ Play';
}
}
}
// Function to reset to start
function resetToStart() {
stopAnimation();
currentYearIndex = 0;
if (allYears.length > 0) {
var firstYear = allYears[0];
animateToYear(firstYear);
updateStats(firstYear);
var picker = document.getElementById('year-picker');
if (picker) picker.value = firstYear;
}
}
// Function to store original marker data
function storeOriginalMarkerData() {
if (!originalMarkerData && gd.data && gd.data[0]) {
originalMarkerData = {
opacity: gd.data[0].marker.opacity,
line: JSON.parse(JSON.stringify(gd.data[0].marker.line))
};
}
}
// Function to highlight specific countries
function highlightCountries(year, countries) {
if (!gd.data || !countries || countries.length === 0) {
return;
}
storeOriginalMarkerData();
// Get ALL current visible data across ALL traces (regions)
var allOpacities = [];
var allLineWidths = [];
var allLineColors = [];
var totalHighlighted = 0;
// Loop through ALL traces (one per region)
for (var traceIndex = 0; traceIndex < gd.data.length; traceIndex++) {
var trace = gd.data[traceIndex];
var textData = trace.text;
if (!textData) continue;
var traceOpacities = [];
var traceLineWidths = [];
var traceLineColors = [];
// Check each point in this trace
for (var i = 0; i < textData.length; i++) {
var text = textData[i];
var isHighlighted = false;
// Extract country name from hover text
var match = text.match(/<b>(.*?)<\\/b>/);
if (match) {
var countryName = match[1];
// Enhanced country matching with more variations
isHighlighted = countries.some(function(country) {
return countryName === country ||
countryName.includes(country) ||
country.includes(countryName) ||
// Specific mappings for common variations
(country === 'China' && (countryName === 'China' || countryName === 'People\\'s Republic of China')) ||
(country === 'United States' && (countryName === 'United States' || countryName === 'USA' || countryName === 'US')) ||
(country === 'Hong Kong' && (countryName.includes('Hong Kong') || countryName === 'Hong Kong SAR China')) ||
(country === 'South Korea' && (countryName.includes('Korea') && !countryName.includes('North'))) ||
(country === 'Russia' && (countryName.includes('Russian') || countryName === 'Russia')) ||
(country === 'Egypt' && countryName.includes('Egypt')) ||
(country === 'Yemen' && countryName.includes('Yemen')) ||
(country === 'Syria' && countryName.includes('Syria')) ||
(country === 'Democratic Republic of Congo' && (countryName.includes('Congo') && countryName.includes('Dem'))) ||
(country === 'Congo, Dem. Rep.' && (countryName.includes('Congo') && countryName.includes('Dem')));
});
}
if (isHighlighted) {
traceOpacities.push(1.0);
traceLineWidths.push(4);
traceLineColors.push('#d62728'); // Red highlight
totalHighlighted++;
} else {
traceOpacities.push(0.15); // More faded non-highlighted countries
traceLineWidths.push(1);
traceLineColors.push('gray');
}
}
allOpacities.push(traceOpacities);
allLineWidths.push(traceLineWidths);
allLineColors.push(traceLineColors);
}
// Debug: Log highlighting info
console.log('Year:', year, 'Expected countries:', countries, 'Total highlighted across all regions:', totalHighlighted);
// Apply highlighting to ALL traces
var updateObj = {
'marker.opacity': allOpacities,
'marker.line.width': allLineWidths,
'marker.line.color': allLineColors
};
Plotly.restyle(gd, updateObj);
}
// FIXED: Function to reset highlighting
function resetHighlighting() {
if (!gd.data || !originalMarkerData) {
return;
}
// Reset ALL traces, not just the first one
var resetOpacities = [];
var resetLineWidths = [];
var resetLineColors = [];
for (var i = 0; i < gd.data.length; i++) {
resetOpacities.push(originalMarkerData.opacity);
resetLineWidths.push(originalMarkerData.line.width);
resetLineColors.push(originalMarkerData.line.color);
}
Plotly.restyle(gd, {
'marker.opacity': resetOpacities,
'marker.line.width': resetLineWidths,
'marker.line.color': resetLineColors
});
}
// Function to update annotations with milestones
function updateAnnotations(year, showMilestone) {
var currentAnnotations = [];
// Add existing axis annotations (HDI and GDP income labels)
if (gd.layout && gd.layout.annotations) {
currentAnnotations = gd.layout.annotations.filter(function(ann) {
// Keep only the axis labels, exclude any previous milestone annotations
return ann.text && (ann.text.includes('HDI') || ann.text.includes('income')) &&
!ann.text.includes('Crisis') && !ann.text.includes('Spring') &&
!ann.text.includes('Collapse') && !ann.text.includes('Pandemic') &&
!ann.text.includes('Disaster') && !ann.text.includes('Decision') &&
!ann.text.includes('Wave') && !ann.text.includes('Financial') &&
!ann.text.includes('Migration');
});
}
// Add milestone annotation at bottom right of the plot area
if (showMilestone && milestoneTexts[year]) {
currentAnnotations.push({
text: milestoneTexts[year],
x: 0.98, y: 0.02,
xref: 'paper', yref: 'paper',
xanchor: 'right', yanchor: 'bottom',
showarrow: false,
font: {size: 11, color: 'darkred', family: 'Arial'},
bgcolor: 'rgba(255,255,255,0.95)',
bordercolor: 'darkred',
borderwidth: 2,
borderpad: 8
});
}
Plotly.relayout(gd, {annotations: currentAnnotations});
}
// Function to animate to specific year
function animateToYear(year) {
Plotly.animate(gd, [year.toString()], {
transition: {duration: 300},
frame: {duration: 0, redraw: false}
}).then(function() {
// Apply highlighting after animation if in timeline mode and it's a milestone
if (isTimelineMode && milestoneYears.includes(year) && countryGroups[year.toString()]) {
setTimeout(function() {
highlightCountries(year, countryGroups[year.toString()]);
}, 100);
}
});
updateAnnotations(year, isTimelineMode && milestoneYears.includes(year));
}
// Custom animation function
function customAnimate() {
if (!isPlaying || currentYearIndex >= allYears.length) {
stopAnimation();
return;
}
var currentYear = allYears[currentYearIndex];
animateToYear(currentYear);
// Update stats and picker
updateStats(currentYear);
var picker = document.getElementById('year-picker');
if (picker) picker.value = currentYear;
// Check if this is a milestone year (only in timeline mode)
var isMilestone = isTimelineMode && milestoneYears.includes(currentYear);
var delay = isMilestone ? 8000 : 800; // 8s for milestone (longer to see highlighting), 0.8s for regular
currentYearIndex++;
animationTimeout = setTimeout(customAnimate, delay);
}
// Function to start animation
function startAnimation(timelineMode, buttonElement) {
// Stop any existing animation first
stopAnimation();
// Only reset to start if we're switching modes or not currently paused at a position
if (isTimelineMode !== timelineMode || currentYearIndex === 0) {
currentYearIndex = 0;
}
isTimelineMode = timelineMode;
// Start new animation
isPlaying = true;
// Update button text
if (timelineMode) {
buttonElement.textContent = '⏸ Pause Timeline';
} else {
buttonElement.textContent = '⏸ Pause';
}
customAnimate();
}
// Override both play buttons behavior - now looking for bottom buttons only
function setupCustomControls() {
// Remove any existing top buttons
var topButtons = document.querySelectorAll('.updatemenu-button');
topButtons.forEach(function(btn) {
if (btn.closest('.js-plotly-plot')) {
btn.style.display = 'none';
}
});
// Setup bottom custom buttons
setupBottomButtons();
}
function setupBottomButtons() {
var timelineBtn = document.getElementById('timeline-btn');
var playBtn = document.getElementById('play-btn');
if (timelineBtn) {
timelineBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
if (isPlaying && isTimelineMode) {
stopAnimation();
} else {
startAnimation(true, this);
}
};
}
if (playBtn) {
playBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
if (isPlaying && !isTimelineMode) {
stopAnimation();
} else {
startAnimation(false, this);
}
};
}
if (!timelineBtn || !playBtn) {
setTimeout(setupBottomButtons, 100);
}
}
// Setup custom controls after plotly renders
setTimeout(setupCustomControls, 1000);
// --- Original stats panel functionality ---
var picker = document.getElementById('year-picker');
if (picker && picker.options.length === 0 && years && years.length) {
years.forEach(function(y) {
var opt = document.createElement('option');
opt.value = y;
opt.text = y;
picker.appendChild(opt);
});
picker.onchange = function() {
// Stop any running animation when user manually selects year
stopAnimation();
var selectedYear = parseInt(this.value);
updateStats(selectedYear);
// Check if manually selected year is a milestone and we're in timeline mode
var shouldShowMilestone = isTimelineMode && milestoneYears.includes(selectedYear);
updateAnnotations(selectedYear, shouldShowMilestone);
// Apply highlighting if in timeline mode and it's a milestone year
if (shouldShowMilestone && countryGroups[selectedYear.toString()]) {
setTimeout(function() {
highlightCountries(selectedYear, countryGroups[selectedYear.toString()]);
}, 100);
} else {
resetHighlighting(); // Reset highlighting for non-milestone years or regular mode
}
if (gd) {
Plotly.animate(gd, {frame: {redraw: true, name: this.value}}, {mode: 'immediate'});
}
};
}
var sl = document.getElementById('size-legend');
if (sl && !sl.innerHTML) {
sl.style.background = 'rgba(255,255,255,0.97)';
sl.style.borderRadius = '8px';
sl.style.boxShadow = '0 0 6px rgba(0,0,0,0.10)';
sl.style.fontSize = '13px';
sl.style.padding = '10px 10px';
sl.innerHTML =
'<div><strong>Circles sized by</strong><br>Population</div>' +
'<div style=\"margin-top:6px;display:flex;align-items:center;gap:6px;\">' +
'<div style=\"width:20px;height:20px;border-radius:50%;background:lightgray;\"></div>' +
'<span>600 M</span>' +
'</div>' +
'<div style=\"margin-top:4px;display:flex;align-items:center;gap:6px;\">' +
'<div style=\"width:30px;height:30px;border-radius:50%;background:lightgray;\"></div>' +
'<span>1.4 B</span>' +
'</div>';
}
function updateStats(year) {
var sb = document.getElementById('stats-box');
if (!sb) return;
var sub = stats.filter(d => +d.Year == +year);
var devo = sub.find(d => d.dev_status=='Developing'),
dev = sub.find(d => d.dev_status=='Developed');
if (!devo || !dev) {
sb.innerHTML = '<span style=\"color:red;\">No data for year ' + year + '</span>';
return;
}
sb.style.background = 'rgba(255,255,255,0.98)';
sb.style.borderRadius = '10px';
sb.style.boxShadow = '0 2px 10px rgba(0,0,0,0.11)';
sb.style.fontSize = '14px';
sb.style.padding = '12px 12px';
sb.innerHTML =
'<b>Year: ' + year + '</b><br>' +
'<b>Developing</b><br>' +
devo.count + ' countries<br>' +
'Avg GDP: $' + (+devo.avg_gdp).toLocaleString() + '<br>' +
'Avg HDI: ' + (+devo.avg_hdi).toFixed(3) + '<br><br>' +
'<b>Developed</b><br>' +
dev.count + ' countries<br>' +
'Avg GDP: $' + (+dev.avg_gdp).toLocaleString() + '<br>' +
'Avg HDI: ' + (+dev.avg_hdi).toFixed(3);
}
// Initialize with first year
if (picker && years && years.length) {
picker.value = years[0];
updateStats(years[0]);
updateAnnotations(years[0], false);
if (gd) {
Plotly.animate(gd, {frame: {redraw: true, name: years[0]}}, {mode: 'immediate'});
}
}
}
")